package rs.udd.service.document.indexer;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.lucene.document.Document;
import org.apache.lucene.index.DirectoryReader;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.index.IndexWriterConfig;
import org.apache.lucene.index.IndexWriterConfig.OpenMode;
import org.apache.lucene.index.IndexableField;
import org.apache.lucene.index.Term;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.ScoreDoc;
import org.apache.lucene.search.TermQuery;
import org.apache.lucene.search.TopScoreDocCollector;
import org.apache.lucene.store.Directory;
import org.apache.lucene.store.SimpleFSDirectory;
import org.apache.lucene.util.Version;
import org.apache.pdfbox.pdfparser.PDFParser;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDDocumentInformation;
import org.apache.pdfbox.util.PDFTextStripper;

import rs.udd.exceptions.IncompleteIndexDocumentException;
import rs.udd.exceptions.MissingMetaDataException;
import rs.udd.service.document.analyzer.SerbianAnalyzer;
import rs.udd.service.document.indexer.handler.DocumentHandler;
import rs.udd.service.document.indexer.handler.PDFHandler;
import rs.udd.service.dto.advert.AdvertDto;
import rs.udd.service.util.FileUtil;

public class UDDIndexer {

	private static final Logger LOGGER = LogManager.getLogger(UDDIndexer.class);
	
	private static final Version matchVersion = Version.LUCENE_4_9;
	private IndexWriter indexWriter;
	private Directory indexDir;
	private String path;

	/**
	 * Starts a custom indexer
	 * 
	 * @param path
	 *            - indexer location on disc
	 * @param restart
	 *            - true to start a new indexer, false to merge with old indexer if present in path folder
	 */
	public UDDIndexer(String path, boolean restart) {
		this.path = path;
		IndexWriterConfig iwc = new IndexWriterConfig(matchVersion, new SerbianAnalyzer(matchVersion));
		if (restart) {
			iwc.setOpenMode(OpenMode.CREATE);
		} else {
			iwc.setOpenMode(OpenMode.CREATE_OR_APPEND);
		}

		try {
			this.indexDir = new SimpleFSDirectory(new File(path));
			this.indexWriter = new IndexWriter(indexDir, iwc);
		} catch (IOException ioe) {
			throw new IllegalArgumentException("Path not correct");
		}
	}

	/**
	 * Starts an indexer at predefined location
	 * 
	 * @param restart
	 *            - true to start a new indexer, false to merge with old indexer if present in path folder
	 */
	public UDDIndexer(boolean restart) {
		this(FileUtil.getIndicesPath(), restart);
	}

	/**
	 * Starts an indexer at given location or merges with an existing one
	 * 
	 * @param path
	 *            - indexer location on disc
	 */
	public UDDIndexer(String path) {
		this(path, false);
	}

	/**
	 * Starts an indexer at predefined location or merges with an existing one
	 */
	public UDDIndexer() {
		this(FileUtil.getIndicesPath(), false);
	}

	public void restart() {
		IndexWriterConfig iwc = new IndexWriterConfig(matchVersion, new SerbianAnalyzer(matchVersion));
		iwc.setOpenMode(OpenMode.CREATE);
		try {
			this.close();
			this.indexDir = new SimpleFSDirectory(new File(path));
			this.indexWriter = new IndexWriter(indexDir, iwc);
		} catch (IOException ioe) {
			throw new IllegalArgumentException("Path not correct");
		}
	}

	public void close() {
		try {
			this.indexWriter.commit();
			this.indexWriter.deleteUnusedFiles();
			this.indexWriter.forceMergeDeletes();
			this.indexWriter.commit();
			this.indexDir.close();
			this.indexWriter.close(true);
		} catch (IOException ioe) {
			throw new IllegalArgumentException("Error while closing indexer: " + ioe.getMessage());
		}
	}

	public IndexWriter getIndexWriter() {
		return this.indexWriter;
	}

	public Directory getIndexDir() {
		return this.indexDir;
	}

	public String getPath() {
		return this.path;
	}

	/**
	 * 
	 * @param doc
	 *            - document which will be indexed
	 * @return - true if adding was successful
	 */
	public boolean addDocument(Document doc) { // indeksiranje tacno jednog dokumenta
		try {
			indexWriter.addDocument(doc);
			indexWriter.commit();
		} catch (IOException e) {
			e.printStackTrace();
		}

		return false;
	}

	public void replaceFields(Document doc, IndexableField... fields) {
		for (IndexableField field : fields) {
			doc.removeFields(field.name());
		}
		for (IndexableField field : fields) {
			doc.add(field);
		}
	}

	/**
	 * 
	 * @param doc
	 *            - the document which will be updated
	 * @param fields
	 *            - array of updating fields
	 * @return true if update was successful, othewise false
	 */
	public boolean updateDocument(Document doc, IndexableField... fields) {
		this.replaceFields(doc, fields);
		String id = doc.get(PDFHandler.ID);
		try {
			synchronized (this) {
				this.indexWriter.updateDocument(new Term(PDFHandler.ID, id), doc);
				this.indexWriter.deleteUnusedFiles();
				this.indexWriter.forceMergeDeletes();
				this.indexWriter.commit();
			}
			return true;
		} catch (Exception ex) {
			return false;
		}
	}

	/**
	 * 
	 * @param id
	 *            - ID of the document which will be updated (id given by the indexer)
	 * @param fields
	 *            - array of updating fields
	 * @return - true if update was successful, false otherwise
	 */
	public boolean updateDocument(int id, IndexableField... fields) {
		try {
			DirectoryReader reader = DirectoryReader.open(indexDir);
			return updateDocument(reader.document(id), fields);
		} catch (IOException e) {
			return false;
		}
	}

	/**
	 * Update all documents which contain a field which has a name same as fieldName and a value same as fieldValue
	 * 
	 * @param fieldName
	 *            - name of the field
	 * @param fieldValue
	 *            - value of the field
	 * @param fields
	 *            - array of updating fields
	 * @return
	 */
	public boolean updateDocuments(String fieldName, String fieldValue, IndexableField... fields) {
		try {
			DirectoryReader reader = DirectoryReader.open(indexDir);
			IndexSearcher searcher = new IndexSearcher(reader);
			Query query = new TermQuery(new Term(fieldName, fieldValue));
			TopScoreDocCollector collector = TopScoreDocCollector.create(10, true);
			searcher.search(query, collector);
			ScoreDoc[] docs = collector.topDocs().scoreDocs;
			Document doc;
			for (ScoreDoc sdoc : docs) {
				doc = searcher.doc(sdoc.doc);
				replaceFields(doc, fields);
				this.indexWriter.updateDocument(new Term(PDFHandler.ID, doc.get(PDFHandler.ID)), doc);
			}
			// commit-ovanje promena
			this.indexWriter.deleteUnusedFiles();
			this.indexWriter.forceMergeDeletes();
			this.indexWriter.commit();
			return true;
		} catch (IOException ioe) {
			return false;
		}
	}

	/**
	 * Update all documents which contain a field which has a name same as fieldName and a value same as fieldValue
	 * 
	 * @param query
	 *            - query which will be used to filter documents
	 * @param fields
	 *            - array of updating fields
	 * @return
	 */
	public boolean updateDocuments(Query query, IndexableField... fields) {
		try {
			DirectoryReader reader = DirectoryReader.open(indexDir);
			IndexSearcher searcher = new IndexSearcher(reader);
			TopScoreDocCollector collector = TopScoreDocCollector.create(10, true);
			searcher.search(query, collector);
			ScoreDoc[] docs = collector.topDocs().scoreDocs;
			Document doc;
			for (ScoreDoc sdoc : docs) {
				doc = searcher.doc(sdoc.doc);
				replaceFields(doc, fields);
				this.indexWriter.updateDocument(new Term(PDFHandler.ID, doc.get(PDFHandler.ID)), doc);
			}
			// commit-ovanje promena
			this.indexWriter.deleteUnusedFiles();
			this.indexWriter.forceMergeDeletes();
			this.indexWriter.commit();
			return true;
		} catch (IOException ioe) {
			return false;
		}
	}

	/**
	 * Delete a single document
	 * 
	 * @param doc
	 *            - a document which will be deleted
	 * @return
	 */
	public boolean deleteDocument(Document doc) {
		if (doc == null)
			return false;
		Term delTerm = new Term(PDFHandler.ID, doc.get(PDFHandler.ID));
		try {
			synchronized (this) {
				this.indexWriter.deleteDocuments(delTerm);
				this.indexWriter.deleteUnusedFiles();
				this.indexWriter.forceMergeDeletes();
				this.indexWriter.commit();
			}
			return true;
		} catch (IOException e) {
			return false;
		}
	}

	/**
	 * Delete a single document
	 * 
	 * @param id
	 *            - index id of the document which will be deleted
	 * @return
	 */
	public boolean deleteDocument(int id) {
		try {
			DirectoryReader reader = DirectoryReader.open(indexDir);
			return deleteDocument(reader.document(id));
		} catch (IOException e) {
			return false;
		}
	}

	/**
	 * Delete all documents which have a field named fieldName with value fieldValue
	 * 
	 * @param fieldName
	 *            - name of the field
	 * @param fieldValue
	 *            - value of the field
	 * @return
	 */
	public boolean deleteDocuments(String fieldName, String fieldValue) {
		return this.deleteDocuments(new Term(fieldName, fieldValue));
	}

	/**
	 * Delete all documents that match the given term
	 * 
	 * @param delTerm
	 * @return
	 */
	public boolean deleteDocuments(Term delTerm) {
		return this.deleteDocuments(new TermQuery(delTerm));
	}

	/**
	 * 
	 * @param query
	 *            - the query which will be used to filter documents which will be deleted
	 * @return
	 */
	public boolean deleteDocuments(Query query) {
		try {
			synchronized (this) {
				this.indexWriter.deleteDocuments(query);
				this.indexWriter.deleteUnusedFiles();
				this.indexWriter.forceMergeDeletes();
				this.indexWriter.commit();
			}
			return true;
		} catch (IOException e) {
			return false;
		}
	}

	/**
	 * 
	 * @return all indexed lucene documents
	 */
	public List<Document> getAllDocuments() {
		try {
			DirectoryReader reader = DirectoryReader.open(indexDir);
			List<Document> docs = new ArrayList<Document>();
			for (int i = 0; i < reader.maxDoc(); i++) {
				docs.add(reader.document(i));
			}
			return docs;
		} catch (IOException e) {
			e.printStackTrace();
		}
		return null;
	}
	
	public Document getDocumentById(String id) {
		List<Document> allDocuments = getAllDocuments();
		for (Document doc : allDocuments) {
			if (doc.get(PDFHandler.ID).equals(id))
				return doc;
		}
		return null;
	}

	public boolean index(File folder, String documentId) {
		return index(folder, null, documentId);
	}
	
	public boolean index(File folder, AdvertDto advertDto, String documentId) {
		// za svaki file uzeti odgovarajuci DocumentHandler
		if (!folder.exists())
			return false;
		
		try {
			DocumentHandler handler;
			
			if (folder.isDirectory()) {
				File[] files = folder.listFiles();
				for (File file : files) {
					if (file.isFile()) {
						handler = UDDIndexer.getHandler(file, documentId);
						try {
							if (advertDto == null) {	// CV is uploaded
								this.indexWriter.addDocument(handler.getDocument(file));
							} else {	// Advert is uploaded
								this.indexWriter.addDocument(handler.getDocument(file, advertDto));
							}
						} catch (IncompleteIndexDocumentException e) {
							System.out.println("Document not indexed\n" + file.getCanonicalPath() + "\n Reasons:\n" + e.getMessage() + "\n");
						}
					} else {
						index(file, advertDto, documentId);
					}
				}
				this.indexWriter.commit();
				return true;
			} else {
				handler = UDDIndexer.getHandler(folder, documentId);
				try {
					if (advertDto == null) {
						return this.addDocument(handler.getDocument(folder));
					} else {
						return this.addDocument(handler.getDocument(folder, advertDto));
					}
				} catch (IncompleteIndexDocumentException e) {
					System.out.println(folder.getCanonicalPath() + " -> " + e.getMessage());
					return false;
				}
			}
		} catch (IOException ioe) {
			return false;
		}
	}

	public static DocumentHandler getHandler(File file, String id) {
		// za svaki file uzeti odgovarajuci DocumentHandler
		if (file.isDirectory())
			return null; // ako je u pitanju direktorijum
		DocumentHandler handler = new PDFHandler(id);

		return handler;
	}

	public void parseAndValidate(PDFParser parser, AdvertDto advertDto) throws IOException {
		parser.parse();

		PDDocument pdf = parser.getPDDocument();
		PDDocumentInformation info = pdf.getDocumentInformation();

		String title = info.getTitle();
		if (StringUtils.isEmpty(title)) {
			advertDto.setMetaTitle(null);
		} else {
			advertDto.setMetaTitle(title.trim());
		}
		
		String author = info.getAuthor();
		if (StringUtils.isEmpty(author)) {
			advertDto.setMetaAuthor(null);
		} else {
			advertDto.setMetaAuthor(author.trim());
		}

		String keywords = info.getKeywords();
		if (keywords == null || keywords.split(PDFHandler.META_DATA_SEPARATOR).length == 0) {
			advertDto.setMetaKeywords(null);
		} else {
			advertDto.setMetaKeywords(keywords.trim());
		}
		
		pdf.close();
	}
	
	public void parseAndValidate(PDFParser parser) throws IOException, MissingMetaDataException {
		LOGGER.info("Validating pdf...");
		
		parser.parse();

		PDDocument pdf = parser.getPDDocument();
		PDFTextStripper stripper = new PDFTextStripper("utf-8");
		String text = stripper.getText(pdf);
		if (StringUtils.isEmpty(text)) {
			throw new MissingMetaDataException("Text missing from pdf.");
		}
		
		pdf.close();
		LOGGER.info("Validation successful.");
	}
	
	public static String generateDocumentId() {
		long currentTime = System.currentTimeMillis();
		String docId = Long.toString(currentTime);
		
		return docId;
	}
	
	public boolean deleteDocument(String id) {
		boolean deletionResult = false;
		if (StringUtils.isNotEmpty(id)) {
			
			List<Document> allDocuments = IndexManager.getIndexer().getAllDocuments();
			for (Document doc : allDocuments) {
				String documentId = doc.get(PDFHandler.ID);
				if (documentId.equals(id)) {
					deletionResult = IndexManager.getIndexer().deleteDocument(doc);
					break;
				}
			}
		}
		LOGGER.info("Document deleted: " + deletionResult);
		return deletionResult;
	}
	
}
